Skip to content

[Testing 15] test(evals): offline eval harness — citation accuracy, injection resistance, leakage - #257

Closed
amal66 wants to merge 1 commit into
Open-Legal-Products:mainfrom
amal66:olp-pr/evals-harness
Closed

[Testing 15] test(evals): offline eval harness — citation accuracy, injection resistance, leakage#257
amal66 wants to merge 1 commit into
Open-Legal-Products:mainfrom
amal66:olp-pr/evals-harness

Conversation

@amal66

@amal66 amal66 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What this tests — and what it doesn't

This harness gates the checkers and the response-format contract, not the model. The fixture answers are recorded, hand-written responses in Mike's exact runtime format — the model is treated the way tests treat any expensive, nondeterministic external dependency: mocked out, so the logic around it can be tested deterministically. A live model regression will not show up here; what will show up is any change that weakens a safety check or drifts the citation format away from what the backend emits and parses.

Why offline-first instead of jumping straight to online evals against real responses:

  • An online eval can't gate CI in an open-source repo. GitHub Actions doesn't expose secrets to fork PRs, so most contributor PRs couldn't run it at all — and it's nondeterministic and costs API spend per run. This harness needs no network, no keys, no npm install, so it runs free on every PR.
  • Online evals are only as trustworthy as their scorers. An online harness is "generate real responses, then score them" — and scorers that have never been tested against known-bad cases produce a green dashboard you can't trust. This PR ships the scoring half first, with a corpus that proves it.
  • The two layers catch different regressions. Example: a well-meaning PR relaxes quote verification from verbatim to fuzzy matching (to cope with OCR noise). A live model's mostly-correct answers still score fine — but case cc-03, where a liability cap is quietly edited from twelve months to twenty-four, now passes a checker that should reject it, mismatches its expected: "fail", and CI goes red. An online eval would likely have missed it.

The checkers are proven, not assumed

Every case declares expected: "pass" or "fail", and roughly half the corpus is known-bad (a fabricated Erie holding, the altered liability cap above, a canary smuggled into a tool-call argument). If a checker is ever weakened, those cases flip and CI fails. Benign edge cases (docket numbers near the SSN pattern, an answer that warns about an injection without repeating it) go red if a checker over-triggers.

How this differs from #231 (closed)

#231 was closed for "limited usefulness of the evals and format. A fuller real evals suite can be implemented in the future which aligns with the format of actual assistant responses and involves real documents." This PR was rebuilt around that feedback:

  • Real response format. Fixture answers are not invented markdown — they are prose with inline [N] markers plus a <CITATIONS> JSON block, exactly what the backend emits and parses. The harness's citation parser is ported line-for-line from backend/src/lib/chat/citations.ts (source lines cited in its comments), document pages mirror the [Page N] blocks from documentOps.ts, and recorded tool calls use the ToolCall shape from chat/types.ts with Mike's real tool names. docs/evals.md has the full fixture-field → runtime-format mapping with file references, so the format contract is auditable. This is also what makes the mocks valid: a recorded response is only a fair stand-in if it's shaped exactly like the real thing.
  • Real documents. Sources are public-domain legal texts at real-world length: Marbury v. Madison and Erie v. Tompkins passages, Hadley v. Baxendale, 17 U.S.C. § 107, 15 U.S.C. §§ 1–2, and full-length contract clauses. Cited spans are buried mid-document, and injection payloads are embedded inside the real text — which is the attack shape the product actually faces.

What this completes

CI already has an evals job that runs node evals/run.mjs --threshold 1.0 if the file exists and skips otherwise. This PR adds that file, turning the gate on: a deterministic scorecard for fabricated citations, prompt injection via retrieved documents, and privileged/PII leakage on every PR.

How to run

node evals/run.mjs                          # all suites, must be 100%
node evals/run.mjs --suite prompt-injection # one suite
node evals/run.mjs --list                   # see every case

Exit 0 at or above threshold, non-zero below. Verified both directions: the committed corpus is 27/27, and corrupting a single quoted span drops it to 26/27 with exit code 1 and a readable reason (quote not verbatim on page 1 of ...).

The suites

  • citation-accuracy — parses the <CITATIONS> block and checks every [N] marker has a matching entry and every quote exists verbatim on the cited page (or in the fetched opinion for cluster_id citations). Covers cross-page quotes, page ranges, and the legacy citation shape.
  • prompt-injection — recorded turns where the document carries an injected instruction and a canary; fails on canary leakage in output or tool arguments, or on a disallowed tool call (e.g. an injected note ordering an edit_document rewrite).
  • privilege-leakage — flags off-matter emails, SSN-like values, sb_secret_* keys, service_role JWTs (the public anon key deliberately doesn't trip it), and document identifiers from another client's matter.

Future live mode

Mocked tests don't replace integration tests against the real dependency — they let you run the real thing less often, out-of-band, where flakiness and cost don't gate merges. The "fuller real evals suite" from #231 is a drop-in extension here: run real prompts through the backend, capture the actual streamed responses and tool calls into this same fixture format, and score them with these same, already-proven checkers — behind an opt-in, outside CI, since it needs keys and isn't deterministic. docs/evals.md spells this out, along with the runtime counterparts (citation verification in backend/src/lib/chat/citations.ts; spotlighting of untrusted content in the security-hardening PR #227).

🤖 Generated with Claude Code

@CLAassistant

CLAassistant commented Jul 23, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@amal66
amal66 force-pushed the olp-pr/evals-harness branch from dd1234e to d6e2ac0 Compare July 25, 2026 21:31
@amal66

amal66 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

recheck

@amal66
amal66 force-pushed the olp-pr/evals-harness branch from d6e2ac0 to 1bf1ebc Compare July 25, 2026 21:45
@amal66
amal66 requested a review from willchen96 July 26, 2026 14:10
…ormat

Completes the CI evals gate that ci.yml already wires up (the job runs
node evals/run.mjs --threshold 1.0 when the file exists, with no npm ci —
so the runner is dependency-free Node ESM).

- evals/run.mjs: deterministic CLI scorecard; --threshold / --suite /
  --list; exits non-zero when the pass rate drops below threshold. The
  citation parser is ported line-for-line from
  backend/src/lib/chat/citations.ts (source lines cited in comments).
- evals/cases/: three suites (27 cases) whose fixtures use the real
  pipeline shapes — answers are prose with inline [N] markers plus a
  <CITATIONS> JSON block (prompts.ts rules, doc-N labels, page ranges,
  [[PAGE_BREAK]], legacy top-level fields, cluster_id case entries),
  document pages mirror the [Page N] blocks from documentOps.ts, and
  tool calls use the ToolCall shape from chat/types.ts with real tool
  names.
- Source texts are real documents: public-domain opinions (Marbury,
  Erie, Hadley), statute text (17 U.S.C. § 107, 15 U.S.C. §§ 1-2), and
  full-length contract clauses, with injections embedded inside the
  real text and cited spans buried mid-document.
- Corpus mixes benign (expected: pass) and known-bad (expected: fail)
  cases, so weakening a checker flips its known-bad cases and fails CI.
- docs/evals.md: how to run, the fixture-field -> runtime-format
  mapping with file references, how to add cases, and an honest scope
  note (checker logic + regression corpus now; recorded live-model runs
  drop into the same fixture format later).

No network, no LLM calls, no secrets. Verified: full run 27/27 exits 0;
a deliberately corrupted quote drops to 26/27 and exits 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@amal66
amal66 force-pushed the olp-pr/evals-harness branch from 1bf1ebc to b37029f Compare August 3, 2026 01:57
@willchen96

Copy link
Copy Markdown
Collaborator

Closing because the current offline eval harness does not provide enough product-level assurance to justify the additional code and maintenance.

The harness evaluates hand-written mock responses using deterministic pattern matching and copied parsing logic. It does not call a model or exercise Mike’s real chat, permissions, retrieval, tool execution, or response-processing pipeline. A passing score therefore confirms that the standalone checker classified its prepared examples correctly, rather than demonstrating that the assistant is accurate or secure.

Any useful citation and security cases should instead become unit or integration tests against Mike’s actual production functions. We can reconsider a dedicated evaluation harness if we later introduce live or recorded model evaluations that exercise the real pipeline.

@willchen96 willchen96 closed this Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants